UpptÀck avancerade mönster för typsÀker formulÀrvalidering för att bygga robusta, felfria applikationer. Guiden tÀcker tekniker för globala utvecklare.
BemÀstra TypsÀker Formularhantering: En Guide till Valideringsmönster
Inom webbutveckling Àr formulÀr det kritiska grÀnssnittet mellan anvÀndare och vÄra applikationer. De Àr portarna för registrering, datainlÀmning, konfiguration och otaliga andra interaktioner. Trots att det Àr en sÄ fundamental komponent Àr hanteringen av formulÀrdata en ökÀnd kÀlla till buggar, sÀkerhetsbrister och frustrerande anvÀndarupplevelser. Vi har alla varit dÀr: ett formulÀr som kraschar vid ovÀntad inmatning, ett backend-system som fallerar pÄ grund av en datamismatch, eller en anvÀndare som undrar varför deras inlÀmning avvisades. Roten till detta kaos ligger ofta i ett enda, genomgripande problem: frÄnkopplingen mellan datastruktur, valideringslogik och applikationens tillstÄnd.
Det Àr hÀr typsÀkerhet revolutionerar spelplanen. Genom att gÄ bortom enkla körtidskontroller och anamma ett typcentrerat tillvÀgagÄngssÀtt kan vi bygga formulÀr som inte bara Àr funktionella, utan bevisligen korrekta, robusta och underhÄllbara. Denna artikel Àr en djupdykning i moderna mönster för typsÀker formulÀrhantering. Vi kommer att utforska hur man skapar en enda sanningskÀlla (single source of truth) för din datas form och regler, vilket eliminerar redundans och sÀkerstÀller att dina frontend-typer och valideringslogik aldrig Àr osynkroniserade. Oavsett om du arbetar med React, Vue, Svelte eller nÄgot annat modernt ramverk, kommer dessa principer att ge dig kraften att skriva renare, sÀkrare och mer förutsÀgbar formulÀrkod för en global anvÀndarbas.
BrÀckligheten i Traditionell Formularvalidering
Innan vi utforskar lösningen Àr det avgörande att förstÄ begrÀnsningarna med konventionella metoder. I Äratal har utvecklare hanterat formulÀrvalidering genom att sy ihop olika logikdelar, vilket ofta leder till ett brÀckligt och felbenÀget system. LÄt oss bryta ner denna traditionella modell.
FormulÀrlogikens Tre Silor
I en typisk, icke-typsÀker installation Àr formulÀrlogiken fragmenterad över tre distinkta omrÄden:
- Typdefinitionen ('Vad'): Detta Àr vÄrt kontrakt med kompilatorn. I TypeScript Àr det ett `interface` eller en `type`-alias som beskriver den förvÀntade formen pÄ formulÀrdatan.
// Den avsedda formen pÄ vÄr data interface UserProfile { username: string; email: string; age?: number; // Valfri Älder website: string; } - Valideringslogiken ('Hur'): Detta Àr en separat uppsÀttning regler, vanligtvis en funktion eller en samling villkorliga kontroller, som körs vid körtid för att upprÀtthÄlla begrÀnsningar pÄ anvÀndarens inmatning.
// En separat funktion för att validera datan function validateProfile(data) { const errors = {}; if (!data.username || data.username.length < 3) { errors.username = 'AnvÀndarnamnet mÄste vara minst 3 tecken.'; } if (!data.email || !/\S+@\S+\.\S+/.test(data.email)) { errors.email = 'Ange en giltig e-postadress.'; } if (data.age && (isNaN(data.age) || data.age < 18)) { errors.age = 'Du mÄste vara minst 18 Är gammal.'; } // Denna kontrollerar inte ens om webbplatsen Àr en giltig URL! return errors; } - Server-sidans DTO/Modell ('Backend-Vad'): Backend har sin egen representation av datan, ofta ett Data Transfer Object (DTO) eller en databasmodell. Detta Àr Ànnu en definition av samma datastruktur, ofta skriven i ett annat sprÄk eller ramverk.
Fragmenteringens Oundvikliga Konsekvenser
Denna separation skapar ett system som Àr som gjort för att misslyckas. Kompilatorn kan kontrollera att du skickar ett objekt som ser ut som `UserProfile` till din valideringsfunktion, men den har inget sÀtt att veta om `validateProfile`-funktionen faktiskt upprÀtthÄller de regler som antyds av `UserProfile`-typen. Detta leder till flera kritiska problem:
- Logik och Typer Glider IsÀr: Det vanligaste problemet. En utvecklare uppdaterar `UserProfile`-interfacet för att göra `age` till ett obligatoriskt fÀlt men glömmer att uppdatera `validateProfile`-funktionen. Koden kompilerar fortfarande, men nu kan din applikation skicka in ogiltig data. Typen sÀger en sak, men körtidslogiken gör en annan.
- Dubbelarbete: Valideringslogiken för frontend mÄste ofta implementeras pÄ nytt i backend för att sÀkerstÀlla dataintegritet. Detta bryter mot DRY-principen (Don't Repeat Yourself) och fördubblar underhÄllsbördan. En Àndring i kraven innebÀr att man mÄste uppdatera kod pÄ minst tvÄ stÀllen.
- Svaga Garantier: `UserProfile`-typen definierar `age` som en `number`, men HTML-formulÀrfÀlt ger strÀngar. Valideringslogiken mÄste komma ihÄg att hantera denna konvertering. Om den inte gör det, kan du skicka `"25"` till ditt API istÀllet för `25`, vilket leder till subtila buggar som Àr svÄra att spÄra.
- DÄlig Utvecklarupplevelse: Utan ett enhetligt system mÄste utvecklare stÀndigt korsreferera flera filer för att förstÄ ett formulÀrs beteende. Denna mentala belastning saktar ner utvecklingen och ökar risken för misstag.
Paradigm-skiftet: Schema-först Validering
Lösningen pÄ denna fragmentering Àr ett kraftfullt paradigmskifte: istÀllet för att definiera typer och valideringsregler separat, definierar vi ett enda valideringsschema som fungerar som den ultimata sanningskÀllan. FrÄn detta schema kan vi sedan hÀrleda vÄra statiska typer.
Vad Àr ett Valideringsschema?
Ett valideringsschema Àr ett deklarativt objekt som definierar formen, datatyperna och begrÀnsningarna för din data. Du skriver inte `if`-satser; du beskriver vad datan bör vara. Bibliotek som Zod, Valibot, Yup och Joi Àr utmÀrkta för detta.
I resten av denna artikel kommer vi att anvÀnda Zod för vÄra exempel pÄ grund av dess utmÀrkta TypeScript-stöd, tydliga API och vÀxande popularitet. Mönstren som diskuteras Àr dock tillÀmpliga Àven pÄ andra moderna valideringsbibliotek.
LÄt oss skriva om vÄrt `UserProfile`-exempel med Zod:
import { z } from 'zod';
// Den enda kÀllan till sanning
const UserProfileSchema = z.object({
username: z.string().min(3, { message: "AnvÀndarnamnet mÄste vara minst 3 tecken." }),
email: z.string().email({ message: "Ogiltig e-postadress." }),
age: z.number().min(18, { message: "Du mÄste vara minst 18 Är." }).optional(),
website: z.string().url({ message: "Ange en giltig URL." }),
});
// HÀrled TypeScript-typen direkt frÄn schemat
type UserProfile = z.infer;
/*
Denna genererade 'UserProfile'-typ Àr ekvivalent med:
type UserProfile = {
username: string;
email: string;
age?: number | undefined;
website: string;
}
Den Àr alltid i synk med valideringsreglerna!
*/
Fördelarna med Schema-först-metoden
- En Enda SanningskÀlla (SSOT): `UserProfileSchema` Àr nu den enda platsen dÀr vi definierar vÄrt datakontrakt. Varje Àndring hÀr Äterspeglas automatiskt i bÄde vÄr valideringslogik och vÄra TypeScript-typer.
- Garanterad Konsekvens: Det Àr nu omöjligt för typen och valideringslogiken att glida isÀr. `z.infer`-verktyget sÀkerstÀller att vÄra statiska typer Àr en perfekt spegelbild av vÄra körtidsvalideringsregler. Om du tar bort `.optional()` frÄn `age`, kommer TypeScript-typen `UserProfile` omedelbart att Äterspegla att `age` Àr en obligatorisk `number`.
- Rik Utvecklarupplevelse: Du fÄr utmÀrkt autokomplettering och typkontroll i hela din applikation. NÀr du kommer Ät datan efter en lyckad validering, vet TypeScript den exakta formen och typen för varje fÀlt.
- LÀsbarhet och UnderhÄllbarhet: Scheman Àr deklarativa och lÀtta att lÀsa. En ny utvecklare kan titta pÄ schemat och omedelbart förstÄ datakraven utan att behöva dechiffrera komplex imperativ kod.
GrundlÀggande Valideringsmönster med Scheman
Nu nÀr vi förstÄr 'varför', lÄt oss dyka in i 'hur'. HÀr Àr nÄgra vÀsentliga mönster för att bygga robusta formulÀr med en schema-först-metod.
Mönster 1: GrundlÀggande och Komplex FÀltvalidering
Schemabibliotek erbjuder en rik uppsÀttning inbyggda valideringsprimitiver som du kan kedja ihop för att skapa precisa regler.
import { z } from 'zod';
const RegistrationSchema = z.object({
// En obligatorisk strÀng med min/max lÀngd
fullName: z.string().min(2, 'Hela namnet Àr för kort').max(100, 'Hela namnet Àr för lÄngt'),
// Ett nummer som mÄste vara ett heltal och inom ett visst intervall
invitationCode: z.number().int().positive('Koden mÄste vara ett positivt tal'),
// En boolean som mÄste vara true (för kryssrutor som "Jag godkÀnner villkoren")
agreedToTerms: z.literal(true, {
errorMap: () => ({ message: 'Du mÄste godkÀnna villkoren.' })
}),
// En enum för en select-lista
accountType: z.enum(['personal', 'business']),
// Ett valfritt fÀlt
bio: z.string().max(500).optional(),
});
type RegistrationForm = z.infer;
Detta enda schema definierar en komplett uppsĂ€ttning regler. Meddelandena som Ă€r kopplade till varje valideringsregel ger tydlig, anvĂ€ndarvĂ€nlig feedback. Notera hur vi kan hantera olika inmatningstyper â text, siffror, booleans och listor â allt inom samma deklarativa struktur.
Mönster 2: Hantering av NÀstlade Objekt och Listor
Verkliga formulÀr Àr sÀllan platta. Scheman gör det trivialt att hantera komplexa, nÀstlade datastrukturer som adresser, eller listor av objekt som fÀrdigheter eller telefonnummer.
import { z } from 'zod';
const AddressSchema = z.object({
street: z.string().min(5, 'Gatuadress krÀvs.'),
city: z.string().min(2, 'Stad krÀvs.'),
postalCode: z.string().regex(/^[0-9]{5}(?:-[0-9]{4})?$/, 'Ogiltigt postnummerformat.'),
country: z.string().length(2, 'AnvÀnd landskoden med 2 bokstÀver.'),
});
const SkillSchema = z.object({
id: z.string().uuid(),
name: z.string(),
proficiency: z.enum(['beginner', 'intermediate', 'expert']),
});
const CompanyProfileSchema = z.object({
companyName: z.string().min(1),
contactEmail: z.string().email(),
billingAddress: AddressSchema, // NĂ€stlar adresschemat
shippingAddress: AddressSchema.optional(), // NÀstling kan ocksÄ vara valfri
skillsNeeded: z.array(SkillSchema).min(1, 'Ange minst en nödvÀndig fÀrdighet.'),
});
type CompanyProfile = z.infer;
I detta exempel har vi komponerat scheman. `CompanyProfileSchema` ÄteranvÀnder `AddressSchema` för bÄde fakturerings- och leveransadresser. Det definierar ocksÄ `skillsNeeded` som en lista dÀr varje element mÄste överensstÀmma med `SkillSchema`. Den hÀrledda `CompanyProfile`-typen kommer att vara perfekt strukturerad med alla nÀstlade objekt och listor korrekt typade.
Mönster 3: Avancerad Villkorlig och KorsfÀltvalidering
Det Àr hÀr schemabaserad validering verkligen briljerar, och lÄter dig hantera dynamiska formulÀr dÀr ett fÀlts krav beror pÄ ett annats vÀrde.
Villkorlig Logik med `discriminatedUnion`
FörestÀll dig ett formulÀr dÀr en anvÀndare kan vÀlja sin notifieringsmetod. Om de vÀljer 'E-post' ska ett e-postfÀlt visas och vara obligatoriskt. Om de vÀljer 'SMS' ska ett telefonnummerfÀlt bli obligatoriskt.
import { z } from 'zod';
const NotificationSchema = z.discriminatedUnion('method', [
z.object({
method: z.literal('email'),
emailAddress: z.string().email(),
}),
z.object({
method: z.literal('sms'),
phoneNumber: z.string().min(10, 'Ange ett giltigt telefonnummer.'),
}),
z.object({
method: z.literal('none'),
}),
]);
type NotificationPreferences = z.infer;
// Exempel pÄ giltig data:
// const byEmail: NotificationPreferences = { method: 'email', emailAddress: 'test@example.com' };
// const bySms: NotificationPreferences = { method: 'sms', phoneNumber: '1234567890' };
// Exempel pÄ ogiltig data (kommer att misslyckas med valideringen):
// const invalid = { method: 'email', phoneNumber: '1234567890' };
`discriminatedUnion` Àr perfekt för detta. Det tittar pÄ `method`-fÀltet och, baserat pÄ dess vÀrde, tillÀmpar det korrekta motsvarande schemat. Den resulterande TypeScript-typen Àr en vacker union-typ som lÄter dig sÀkert kontrollera `method` och veta vilka andra fÀlt som Àr tillgÀngliga.
KorsfÀltvalidering med `superRefine`
Ett klassiskt formulÀrkrav Àr lösenordsbekrÀftelse. FÀlten `password` och `confirmPassword` mÄste matcha. Detta kan inte valideras pÄ ett enskilt fÀlt; det krÀver en jÀmförelse av tvÄ. Zods `.superRefine()` (eller `.refine()` pÄ objektet) Àr verktyget för detta jobb.
import { z } from 'zod';
const PasswordChangeSchema = z.object({
password: z.string().min(8, 'Lösenordet mÄste vara minst 8 tecken lÄngt.'),
confirmPassword: z.string(),
})
.superRefine(({ confirmPassword, password }, ctx) => {
if (confirmPassword !== password) {
ctx.addIssue({
code: 'custom',
message: 'Lösenorden matchade inte',
path: ['confirmPassword'], // FĂ€ltet som felet ska kopplas till
});
}
});
type PasswordChangeForm = z.infer;
`superRefine`-funktionen tar emot det fullstÀndigt parsade objektet och en kontext (`ctx`). Du kan lÀgga till anpassade fel (issues) till specifika fÀlt, vilket ger dig full kontroll över komplexa affÀrsregler som spÀnner över flera fÀlt.
Mönster 4: Transformering och Tvingande av Data
FormulÀr pÄ webben hanterar strÀngar. En anvÀndare som skriver '25' i ett `` producerar fortfarande ett strÀngvÀrde. Ditt schema bör ansvara för att konvertera denna rÄa indata till den rena, korrekt typade data som din applikation behöver.
import { z } from 'zod';
const EventCreationSchema = z.object({
eventName: z.string().trim().min(1), // Trimma blanksteg före validering
// Tvinga en strÀng frÄn ett input-fÀlt till ett nummer
capacity: z.coerce.number().int().positive('Kapaciteten mÄste vara ett positivt tal.'),
// Tvinga en strÀng frÄn ett datum-input till ett Date-objekt
startDate: z.coerce.date(),
// Transformera indata till ett mer anvÀndbart format
tags: z.string().transform(val =>
val.split(',').map(tag => tag.trim())
), // t.ex., "tech, global, conference" -> ["tech", "global", "conference"]
});
type EventData = z.infer;
HÀr Àr vad som hÀnder:
- `.trim()`: En enkel men kraftfull transformation som rensar upp strÀngindata.
- `z.coerce`: Detta Àr en speciell Zod-funktion som först försöker tvinga indatan till den angivna typen (t.ex. `"123"` till `123`) och sedan kör valideringarna. Detta Àr avgörande för att hantera rÄ formulÀrdata.
- `.transform()`: För mer komplex logik lÄter `.transform()` dig köra en funktion pÄ vÀrdet efter att det har validerats framgÄngsrikt, och omvandla det till ett mer önskvÀrt format för din applikationslogik.
Integrering med Formularbibliotek: Den Praktiska TillÀmpningen
Att definiera ett schema Àr bara halva striden. För att vara verkligt anvÀndbart mÄste det integreras sömlöst med ditt UI-ramverks bibliotek för formulÀrhantering. De flesta moderna formulÀrbibliotek, som React Hook Form, VeeValidate (för Vue) eller Formik, stöder detta genom ett koncept som kallas en "resolver".
LÄt oss titta pÄ ett exempel med React Hook Form och den officiella Zod-resolvern.
// 1. Installera nödvÀndiga paket
// npm install react-hook-form zod @hookform/resolvers
import React from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
// 2. Definiera vÄrt schema (samma som tidigare)
const UserProfileSchema = z.object({
username: z.string().min(3, "AnvÀndarnamnet Àr för kort"),
email: z.string().email(),
});
// 3. HĂ€rled typen
type UserProfile = z.infer;
// 4. Skapa React-komponenten
export const ProfileForm = () => {
const {
register,
handleSubmit,
formState: { errors }
} = useForm({ // Skicka den hÀrledda typen till useForm
resolver: zodResolver(UserProfileSchema), // Koppla Zod till React Hook Form
});
const onSubmit = (data: UserProfile) => {
// 'data' Àr fullstÀndigt typsÀkert och garanterat giltigt!
console.log('Giltig data skickad:', data);
// t.ex. anropa ett API med denna rena data
};
return (
);
};
Detta Àr ett vackert elegant och robust system. `zodResolver` fungerar som bron. React Hook Form delegerar hela valideringsprocessen till Zod. Om datan Àr giltig enligt `UserProfileSchema` anropas `onSubmit`-funktionen med den rena, typade och eventuellt transformerade datan. Om inte, fylls `errors`-objektet med de exakta meddelanden vi definierade i vÄrt schema.
Bortom Frontend: Full-Stack TypsÀkerhet
Den sanna kraften i detta mönster förverkligas nÀr du utökar det över hela din teknikstack. Eftersom ditt Zod-schema bara Àr ett JavaScript/TypeScript-objekt, kan det delas mellan din frontend- och backend-kod.
En Delad SanningskÀlla
I en modern monorepo-setup (med verktyg som Turborepo, Nx, eller bara Yarn/NPM workspaces), kan du definiera dina scheman i ett delat `common`- eller `core`-paket.
/my-project âââ packages/ â âââ common/ # <-- Delad kod â â âââ src/ â â âââ schemas/ â â âââ user-profile.ts (exporterar UserProfileSchema) â âââ web-app/ # <-- Frontend (t.ex. Next.js, React) â âââ api-server/ # <-- Backend (t.ex. Express, NestJS)
Nu kan bÄde frontend och backend importera exakt samma `UserProfileSchema`-objekt.
- Frontend anvÀnder det med `zodResolver` som visats ovan.
- Backend anvÀnder det i en API-endpoint för att validera inkommande request-bodies.
// Exempel pÄ en Express.js-route i backend
import express from 'express';
import { UserProfileSchema } from 'common/src/schemas/user-profile'; // Importera frÄn delat paket
const app = express();
app.use(express.json());
app.post('/api/profile', (req, res) => {
const validationResult = UserProfileSchema.safeParse(req.body);
if (!validationResult.success) {
// Om valideringen misslyckas, returnera 400 Bad Request med felen
return res.status(400).json({ errors: validationResult.error.flatten() });
}
// Om vi nÄr hit Àr validationResult.data fullstÀndigt typsÀkert och sÀkert att anvÀnda
const cleanData = validationResult.data;
// ... fortsÀtt med databasoperationer, etc.
console.log('Mottog sÀker data pÄ servern:', cleanData);
return res.status(200).json({ message: 'Profil uppdaterad!' });
});
Detta skapar ett okrossbart kontrakt mellan din klient och server. Du har uppnÄtt sann end-to-end typsÀkerhet. Det Àr nu omöjligt för frontend att skicka en datastruktur som backend inte förvÀntar sig, eftersom bÄda validerar mot exakt samma definition.
Avancerade ĂvervĂ€ganden för en Global Publik
Att bygga applikationer för en internationell publik introducerar ytterligare komplexitet. En typsÀker, schema-först-metod ger en utmÀrkt grund för att tackla dessa utmaningar.
Lokalisering (i18n) av Felmeddelanden
Att hÄrdkoda felmeddelanden pÄ engelska Àr inte acceptabelt för en global produkt. Ditt valideringsschema mÄste stödja internationalisering. Zod lÄter dig tillhandahÄlla en anpassad felmappning (error map), som kan integreras med ett standard i18n-bibliotek som `i18next`.
import { z, ZodErrorMap } from 'zod';
import i18next from 'i18next'; // Din i18n-instans
// Denna funktion mappar Zod-felkoder till dina översÀttningsnycklar
const zodI18nMap: ZodErrorMap = (issue, ctx) => {
let message;
// Exempel: översÀtt 'invalid_type'-felet
if (issue.code === 'invalid_type') {
message = i18next.t('validation.invalid_type');
}
// LÀgg till fler mappningar för andra felkoder som 'too_small', 'invalid_string' etc.
else {
message = ctx.defaultError; // Ă
tergÄ till Zods standard
}
return { message };
};
// SÀtt den globala felmappningen för din applikation
z.setErrorMap(zodI18nMap);
// Nu kommer alla scheman att anvÀnda denna mappning för att generera felmeddelanden
const MySchema = z.object({ name: z.string() });
// MySchema.parse(123) kommer nu att producera ett översatt felmeddelande!
Genom att sÀtta en global felmappning vid din applikations startpunkt kan du sÀkerstÀlla att alla valideringsmeddelanden passerar genom ditt översÀttningssystem, vilket ger en sömlös upplevelse för anvÀndare över hela vÀrlden.
Skapa à teranvÀndbara Anpassade Valideringar
Olika regioner har olika dataformat (t.ex. telefonnummer, skatte-ID, postnummer). Du kan kapsla in denna logik i ÄteranvÀndbara schema-förfiningar (refinements).
import { z } from 'zod';
import { isValidPhoneNumber } from 'libphonenumber-js'; // Ett populÀrt bibliotek för detta
// Skapa en ÄteranvÀndbar anpassad validering för internationella telefonnummer
const internationalPhoneNumber = z.string().refine(
(phone) => isValidPhoneNumber(phone),
{
message: 'Ange ett giltigt internationellt telefonnummer.',
}
);
// AnvÀnd den nu i valfritt schema
const ContactSchema = z.object({
name: z.string(),
phone: internationalPhoneNumber,
});
Detta tillvÀgagÄngssÀtt hÄller dina scheman rena och din komplexa, regionspecifika valideringslogik centraliserad och ÄteranvÀndbar.
Slutsats: Bygg med SjÀlvförtroende
Resan frÄn fragmenterad, imperativ validering till en enhetlig, schema-först-metod Àr omvÀlvande. Genom att etablera en enda sanningskÀlla för din datas form och regler eliminerar du hela kategorier av buggar, ökar utvecklarproduktiviteten och skapar en mer motstÄndskraftig och underhÄllbar kodbas.
LÄt oss sammanfatta de djupgÄende fördelarna:
- Robusthet: Dina formulÀr blir mer förutsÀgbara och mindre benÀgna för körtidsfel.
- UnderhÄllbarhet: Logiken Àr centraliserad, deklarativ och lÀtt att förstÄ.
- Utvecklarupplevelse: Njut av statisk analys, autokomplettering och förtroendet att dina typer och valideringar alltid Àr synkroniserade.
- Full-Stack Integritet: Dela scheman mellan klient och server för att skapa ett verkligt okrossbart datakontrakt.
Webben kommer att fortsĂ€tta utvecklas, men behovet av tillförlitligt datautbyte mellan anvĂ€ndare och system kommer att bestĂ„. Att anamma typsĂ€ker, schemadriven formulĂ€rvalidering handlar inte bara om att följa en ny trend; det handlar om att omfamna ett mer professionellt, disciplinerat och effektivt sĂ€tt att bygga programvara. SĂ„ nĂ€sta gĂ„ng du startar ett nytt projekt eller refaktorerar ett gammalt formulĂ€r, uppmuntrar jag dig att strĂ€cka dig efter ett bibliotek som Zod och bygga din grund pĂ„ sĂ€kerheten av ett enda, enhetligt schema. Ditt framtida jag â och dina anvĂ€ndare â kommer att tacka dig.